#ifndef KASCALE_URL_H
#define KASCALE_URL_H

#include <StringUtil.h>
#include <string>
#include <sstream>
#include <map>
#include <iomanip>
using namespace std;
#include "stdlib.h"
namespace kascale
{
	class Url
	{
	public:
		enum UrlScheme
		{
			Unspecified,
			File,
			Ftp,
			Gopher,
			Http,
			Https,
			Mailto,
			News,
			Nntp
		};
		enum UrlHostNameType
		{
			HNT_Basic,			//The host is set, but the type cannot be determined
			HNT_Dns,			//The host name is a Domain Name System(DNS) style host name
			HNT_IPv4,			//The host name is a Internet Protocol(IP) version 4 host address.
			HNT_IPv6,			//The host name is a Internet Protocol(IP) version 6 host address.
			HNT_Unknown			//The type of the host name is not supplied
		};
	//Constructors and destructor
	public:
		Url(const char* str = NULL)
		{
			if(str == NULL)
				url = "";
            else
				url = str;
		}
		Url(const Url& u)
		{
			url = u.url;
		}
		Url(const string& str)
		{
			url = str;
		}
	//Attributes
	public:
		//gets the url string after host info and before query info
		string AbsolutePath() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.path;
			else
				return "";
		}
		//gets the whole url string
		string AbsoluteUrl() const
		{
			return url;
		}
		//gets the host name and port number(if specified and doesn't equal to default port)
		string Authority() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
			{
				if(ud.port == "")
				{
					return ud.host;
				}
				else
				{
					int port = atoi(ud.port.c_str());
					UrlScheme us = GetSchemeFromString(ud.scheme.c_str());
					if(us == Unspecified || DefaultPort(us) != port)
					{
						return ud.host + ":" + ud.port;
					}
					else 
					{
						return ud.host;
					}
				}
			}
			else
				return "";
		}
		//gets the fragment including the starting '#'
		string Fragment() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.fragment;
			else
				return "";
		}
		//gets the host string
		string Host() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.host;
			else
				return "";
		}
		//gets path and query string
		string PathAndQuery() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.path + ud.query;
			else
				return "";
		}
		//gets query string
		string Query() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.query;
			else
				return "";
		}
		//gets the port number
		int	   Port() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
			{
				if(ud.port == "")
				{
					return DefaultPort(GetSchemeFromString(ud.scheme.c_str()));
				}
				else
				{
					return atoi(ud.port.c_str());
				}
			}
			else
				return -1;
		}
		string Scheme() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.scheme;
			else
				return "";
		}
		string UserInfo() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
				return ud.user;
			else
				return "";
		}
		UrlHostNameType HostNameType() const
		{
			//TODO: to be finished
			return HNT_Dns;
		}
		bool IsDefaultPort() const
		{
			UrlData ud;
			if(ParseUrl(url.c_str(), ud))
			{
				if(ud.port == "")
					return true;
				else
					return DefaultPort(GetSchemeFromString(ud.scheme.c_str())) == atoi(ud.port.c_str());
			}
			else
				return true;
		}
		bool IsLoopback() const;
		operator string() const
		{
			return url;
		}
		operator const char*() const
		{
			return url.c_str();
		}
	//Compare
	public:
        bool operator==(const Url& u) const
		{
			return url == u.url;
		}
		bool operator!=(const Url& u) const
		{
			return url != u.url;
		}
		bool operator<(const Url& u) const
		{
			return url < u.url;
		}
	//static members
	public:
		static string EncodePath(const char* url)
		{
			ostringstream ostr;
			ostr << setbase(16) << setiosflags(ios::right);
			unsigned char c = (unsigned char)*url;
			while(c != '\0')
			{
				if(c >= 0x80 || c <= 0x20 || c == '=' || c == '&' || c == '?' || c == '#' || c == '+' || c == '$' || c == '\'' || c == '\"' || c == ')' || c == '(' || c == '*' || c == ',' || c == ';')
					ostr << '%' << setw(2) << setfill('0') << ((unsigned short)c & 0x00FF);
				else
					ostr << c;
				c = *++url;
			}
			return ostr.str();
		}
		static string EncodeQueryString(const char* url)
		{
			ostringstream ostr;
			ostr << setbase(16) << setiosflags(ios::right);
			unsigned char c = (unsigned char)*url;
			while(c != '\0')
			{
				if(c >= 0x80 || c <= 0x20 || c == '=' || c == '&' || c == '?' || c == '#' || c == '+' || c == '$' || c == '\'' || c == '\"' || c == ')' || c == '(' || c == '*' || c == ',' || c == ';' || c == '/' || c == '\\')
					ostr << '%' << setw(2) << setfill('0') << ((unsigned short)c & 0x00FF);
				else
					ostr << c;
				c = *++url;
			}
			return ostr.str();
		}
		static int DefaultPort(UrlScheme us)
		{
			static const int dp[] = { -1, -1, 21, 70, 80, 443, 25, 119, 119};
			return dp[us];
		}
		static UrlScheme GetSchemeFromString(const char* scheme)
		{
			if(strcasecmp(SchemeFile.c_str(), scheme) == 0)
				return File;
			else if(strcasecmp(SchemeFtp.c_str(), scheme) == 0)
				return Ftp;
			else if(strcasecmp(SchemeGopher.c_str(), scheme) == 0)
				return Gopher;
			else if(strcasecmp(SchemeHttp.c_str(), scheme) == 0)
				return Http;
			else if(strcasecmp(SchemeHttps.c_str(), scheme) == 0)
				return Https;
			else if(strcasecmp(SchemeMailto.c_str(), scheme) == 0)
				return Mailto;
			else if(strcasecmp(SchemeNews.c_str(), scheme) == 0)
				return News;
			else if(strcasecmp(SchemeNntp.c_str(), scheme) == 0)
				return Nntp;
			else
				return Unspecified;
		}
		static const string SchemeDelimiter;
		static const string SchemeFile;
		static const string SchemeFtp;
		static const string SchemeGopher;
		static const string SchemeHttp;
		static const string SchemeHttps;
		static const string SchemeMailto;
		static const string SchemeNews;
		static const string SchemeNntp;

		static const Url Null;
		struct UrlData
		{
			string scheme;
			string user;
			string host;
			string port;
			string path;
            string query;
			string fragment;
			void clear()
			{
				scheme = user = host = port = query = fragment = "";
				path = "/";
			}
		};
		static bool ParseUrl(const char* url, UrlData& ud)
		{
			ud.clear();
			string u = url;
			trim(u);
			char *pbef = &u[0], *pnext = &u[0], c;
            //find protocol
			pnext = strstr(pbef, SchemeDelimiter.c_str());
			if(pnext == NULL)
			{
				return true;
			}
			else
			{
				c = *pnext;
				*pnext = '\0';
				ud.scheme = tolower(pbef);
				*pnext = c;
			}
			pnext += SchemeDelimiter.length();
			//find user info
			pbef = pnext;
			pnext = strchr(pbef, '@');
			if(pnext == NULL)
			{
				pnext = pbef;		//ignore user info
			}
			else
			{
				*pnext = '\0';
				ud.user = pbef;
				*pnext = '@';
				++pnext;		//ignore '@'
			}
			//find host
			pbef = pnext;
			if(*pnext == '[')		// ipv6 address
			{
				pnext = strpbrk(pbef, "]");
				if(pnext == NULL)
					return false;
				*pnext = '\0';
				ud.host = pbef + 1;
				*pnext = ']';
				++pnext;
				pbef = pnext;
				if(*pnext == ':')		//there is a port number afterwards
				{
					pnext = strchr(pbef, '/');
					if(pnext == NULL)
					{
						ud.port = pbef + 1;
						return true;
					}
					else
					{
						*pnext = '\0';
						ud.port = pbef + 1;
						*pnext = '/';
					}
				}
			}
			else					// ipv4 or domain name
			{
				pnext = strpbrk(pbef, ":/");
				if(pnext == NULL)		//the string left is merely host name
				{
					ud.host = pbef;
					return true;
				}				
				else if(*pnext == ':')		//there is a port number afterwards
				{
					*pnext = '\0';
					ud.host = pbef;
					*pnext = ':';
					pbef = pnext;
					pnext = strchr(pbef, '/');
					if(pnext == NULL)
					{
						ud.port = pbef + 1;
						return true;
					}
					else
					{
						*pnext = '\0';
						ud.port = pbef + 1;
						*pnext = '/';
					}
				}
				else// *pnext == '/'
				{
					*pnext = '\0';
					ud.host = pbef;
					*pnext = '/';
				}
			}
			//find path
			pbef = pnext;
			pnext = strchr(pbef, '?');
			if(pnext == NULL)
			{
				pnext = strchr(pbef, '#');
				if(pnext == NULL)
				{
					ud.path = pbef;
					return true;
				}
				else
				{
					*pnext = '\0';
					ud.path = pbef;
					*pnext = '#';
					ud.fragment = pnext;
					return true;
				}
			}
			else	//*pnext == '?'
			{
				*pnext = '\0';
				ud.path = pbef;
				*pnext = '?';
			}
			//find query
			pbef = pnext;
			pnext = strchr(pbef, '#');
			if(pnext == NULL)
			{
				ud.query = pbef;
				return true;
			}
			else
			{
				*pnext = '\0';
				ud.query = pbef;
				*pnext ='#';
				ud.fragment = pnext;
			}
			return true;
		}
		static string BuildUrl(const string& path, const map<string, string>& query)
		{
            string str = EncodePath(path.c_str());
			if(!query.empty())
			{
				str += "?";
				map<string, string>::const_iterator it = query.begin();
				for(; it != query.end(); ++it)
				{
					str += EncodeQueryString(it->first.c_str()) + "=" + EncodeQueryString(it->second.c_str()) + "&";
				}
				str.resize(str.size() - 1);			//remove the last &
			}
			return str;
		}
	private:
		string url;
		friend istream & operator>>(istream& istr, Url& url);
		friend ostream& operator<<(ostream& ostr, const Url& url);
	};
	inline istream & operator>>(istream& istr, Url& url)
	{
		url.url.resize(0);
		while(istr && url.url.empty())
			getline(istr, url.url);
		return istr;
	}
	inline ostream& operator<<(ostream& ostr, const Url& url)
	{
		ostr << url.url;
		return ostr;
	}
};			//namespace kascale
#endif // !defined(KASCALE_URL_H)
